/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * ClassDiscovery.java
 * Copyright (C) 2005-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core;

import java.io.File;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * This class is used for discovering classes that implement a certain interface
 * or a derived from a certain class.
 * 
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 * @see StringCompare
 */
public class ClassDiscovery {

    /** whether to output some debug information. */
    public final static boolean VERBOSE = false;

    /**
     * for caching queries (classname+packagename &lt;-&gt; Vector with classnames).
     */
    protected static Hashtable<String, Vector<String>> m_Cache;

    /** the overall class cache. */
    protected static ClassCache m_ClassCache;

    /** notify if VERBOSE is still on */
    static {
        if (VERBOSE) {
            System.err.println(ClassDiscovery.class.getName() + ": VERBOSE ON");
        }
    }

    /**
     * If the given package can be found in this part of the classpath then an URL
     * object is returned, otherwise <code>null</code>.
     * 
     * @param classpathPart the part of the classpath to look for the package
     * @param pkgname       the package to look for
     * @return if found, the url as string, otherwise null
     */
    protected static URL getURL(String classpathPart, String pkgname) {
        String urlStr;
        URL result;
        File classpathFile;
        File file;
        JarFile jarfile;
        Enumeration<JarEntry> enm;
        String pkgnameTmp;

        result = null;
        urlStr = null;

        try {
            classpathFile = new File(classpathPart);

            // directory or jar?
            if (classpathFile.isDirectory()) {
                // does the package exist in this directory?
                file = new File(classpathPart + pkgname);
                if (file.exists()) {
                    urlStr = "file:" + classpathPart + pkgname;
                }
            } else {
                // is package actually included in jar?
                jarfile = new JarFile(classpathPart);
                enm = jarfile.entries();
                pkgnameTmp = pkgname.substring(1); // remove the leading "/"
                while (enm.hasMoreElements()) {
                    if (enm.nextElement().toString().startsWith(pkgnameTmp)) {
                        urlStr = "jar:file:" + classpathPart + "!" + pkgname;
                        break;
                    }
                }
                jarfile.close();
            }
        } catch (Exception e) {
            // ignore
        }

        // try to generate URL from url string
        if (urlStr != null) {
            try {
                result = new URL(urlStr);
            } catch (Exception e) {
                System.err.println("Trying to create URL from '" + urlStr + "' generates this exception:\n" + e);
                result = null;
            }
        }

        return result;
    }

    /**
     * Checks the given packages for classes that inherited from the given class, in
     * case it's a class, or implement this class, in case it's an interface.
     * 
     * @param classname the class/interface to look for
     * @param pkgnames  the packages to search in
     * @return a list with all the found classnames
     */
    public static Vector<String> find(String classname, String[] pkgnames) {
        Vector<String> result;
        Class<?> cls;

        result = new Vector<String>();

        try {
            // cls = Class.forName(classname);
            cls = WekaPackageClassLoaderManager.forName(classname);
            result = find(cls, pkgnames);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result;
    }

    /**
     * Checks the given package for classes that inherited from the given class, in
     * case it's a class, or implement this class, in case it's an interface.
     * 
     * @param classname the class/interface to look for
     * @param pkgname   the package to search in
     * @return a list with all the found classnames
     */
    public static Vector<String> find(String classname, String pkgname) {
        Vector<String> result;
        Class<?> cls;

        result = new Vector<String>();

        try {
            // cls = Class.forName(classname);
            cls = WekaPackageClassLoaderManager.forName(classname);
            result = find(cls, pkgname);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result;
    }

    /**
     * Checks the given packages for classes that inherited from the given class, in
     * case it's a class, or implement this class, in case it's an interface.
     * 
     * @param cls      the class/interface to look for
     * @param pkgnames the packages to search in
     * @return a list with all the found classnames
     */
    public static Vector<String> find(Class<?> cls, String[] pkgnames) {
        Vector<String> result;
        int i;
        HashSet<String> names;

        result = new Vector<String>();

        names = new HashSet<String>();
        for (i = 0; i < pkgnames.length; i++) {
            names.addAll(find(cls, pkgnames[i]));
        }

        // sort result
        result.addAll(names);
        Collections.sort(result, new StringCompare());

        return result;
    }

    /**
     * Find all classes that have the supplied matchText String in their suffix.
     * 
     * @param matchText the text to match
     * @return an array list of matching fully qualified class names.
     */
    public static ArrayList<String> find(String matchText) {
        return m_ClassCache.find(matchText);
    }

    /**
     * Checks the given package for classes that inherited from the given class, in
     * case it's a class, or implement this class, in case it's an interface.
     * 
     * @param cls     the class/interface to look for
     * @param pkgname the package to search in
     * @return a list with all the found classnames
     */
    public static Vector<String> find(Class<?> cls, String pkgname) {
        Vector<String> result;
        int i;
        Class<?> clsNew;

        // already cached?
        result = getCache(cls, pkgname);

        if (result == null) {
            if (VERBOSE) {
                System.out.println("Searching for '" + cls.getName() + "' in '" + pkgname + "':");
            }

            result = new Vector<String>();
            if (m_ClassCache.getClassnames(pkgname) != null) {
                result.addAll(m_ClassCache.getClassnames(pkgname));
            }

            // check classes
            i = 0;
            while (i < result.size()) {
                try {
                    // clsNew = Class.forName(result.get(i));
                    clsNew = WekaPackageClassLoaderManager.forName(result.get(i));

                    // no abstract classes
                    if (Modifier.isAbstract(clsNew.getModifiers())) {
                        m_ClassCache.remove(result.get(i));
                        result.remove(i);
                    }
                    // must implement interface
                    else if ((cls.isInterface()) && (!InheritanceUtils.hasInterface(cls, clsNew))) {
                        result.remove(i);
                    }
                    // must be derived from class
                    else if ((!cls.isInterface()) && (!InheritanceUtils.isSubclass(cls, clsNew))) {
                        result.remove(i);
                    } else {
                        i++;
                    }
                } catch (Exception e) {
                    System.out.println("Accessing class '" + result.get(i) + "' resulted in error:");
                    e.printStackTrace();
                }
            }

            // sort result
            Collections.sort(result, new StringCompare());

            // add to cache
            addCache(cls, pkgname, result);
        }

        return result;
    }

    /**
     * adds all the sub-directories recursively to the list.
     * 
     * @param prefix the path prefix
     * @param dir    the directory to look in for sub-dirs
     * @param list   the current list of sub-dirs
     * @return the new list of sub-dirs
     */
    protected static HashSet<String> getSubDirectories(String prefix, File dir, HashSet<String> list) {
        File[] files;
        int i;
        String newPrefix;

        // add directory to the list
        if (prefix == null) {
            newPrefix = "";
        } else if (prefix.length() == 0) {
            newPrefix = dir.getName();
        } else {
            newPrefix = prefix + "." + dir.getName();
        }

        if (newPrefix.length() != 0) {
            list.add(newPrefix);
        }

        // search for sub-directories
        files = dir.listFiles();
        if (files != null) {
            for (i = 0; i < files.length; i++) {
                if (files[i].isDirectory()) {
                    list = getSubDirectories(newPrefix, files[i], list);
                }
            }
        }

        return list;
    }

    /**
     * Lists all packages it can find in the classpath.
     * 
     * @return a list with all the found packages
     */
    public static Vector<String> findPackages() {
        Vector<String> result;
        Enumeration<String> packages;

        initCache();

        result = new Vector<String>();
        packages = m_ClassCache.packages();
        while (packages.hasMoreElements()) {
            result.add(packages.nextElement());
        }
        Collections.sort(result, new StringCompare());

        return result;
    }

    /**
     * initializes the cache for the classnames.
     */
    protected static void initCache() {
        if (m_Cache == null) {
            m_Cache = new Hashtable<String, Vector<String>>();
        }
        if (m_ClassCache == null) {
            m_ClassCache = new ClassCache();
        }
    }

    /**
     * adds the list of classnames to the cache.
     * 
     * @param cls        the class to cache the classnames for
     * @param pkgname    the package name the classes were found in
     * @param classnames the list of classnames to cache
     */
    protected static void addCache(Class<?> cls, String pkgname, Vector<String> classnames) {
        initCache();
        m_Cache.put(cls.getName() + "-" + pkgname, classnames);
    }

    /**
     * returns the list of classnames associated with this class and package, if
     * available, otherwise null.
     * 
     * @param cls     the class to get the classnames for
     * @param pkgname the package name for the classes
     * @return the classnames if found, otherwise null
     */
    protected static Vector<String> getCache(Class<?> cls, String pkgname) {
        initCache();
        return m_Cache.get(cls.getName() + "-" + pkgname);
    }

    /**
     * clears the cache for class/classnames queries.
     */
    public static void clearCache() {
        initCache();
        m_Cache.clear();
    }

    /**
     * Calls clearCache() and resets the cache of classes on the classpath (i.e.
     * forces a rescan of the classpath).
     */
    public static void clearClassCache() {
        clearCache();
        // make sure that any new classes are picked up
        m_ClassCache = new ClassCache();
    }

    /**
     * Possible calls:
     * <ul>
     * <li>weka.core.ClassDiscovery &lt;packages&gt;<br/>
     * Prints all the packages in the current classpath</li>
     * <li>weka.core.ClassDiscovery &lt;classname&gt; &lt;packagename(s)&gt;<br/>
     * Prints the classes it found.</li>
     * </ul>
     * 
     * @param args the commandline arguments
     */
    public static void main(String[] args) {
        Vector<String> list;
        Vector<String> packages;
        int i;
        StringTokenizer tok;

        if ((args.length == 1) && (args[0].equals("packages"))) {
            list = findPackages();
            for (i = 0; i < list.size(); i++) {
                System.out.println(list.get(i));
            }
        } else if (args.length == 2) {
            // packages
            packages = new Vector<String>();
            tok = new StringTokenizer(args[1], ",");
            while (tok.hasMoreTokens()) {
                packages.add(tok.nextToken());
            }

            // search
            list = ClassDiscovery.find(args[0], packages.toArray(new String[packages.size()]));

            // print result, if any
            System.out.println("Searching for '" + args[0] + "' in '" + args[1] + "':\n" + "  " + list.size() + " found.");
            for (i = 0; i < list.size(); i++) {
                System.out.println("  " + (i + 1) + ". " + list.get(i));
            }
        } else {
            System.out.println("\nUsage:");
            System.out.println(ClassDiscovery.class.getName() + " packages");
            System.out.println("\tlists all packages in the classpath");
            System.out.println(ClassDiscovery.class.getName() + " <classname> <packagename(s)>");
            System.out.println("\tlists classes derived from/implementing 'classname' that");
            System.out.println("\tcan be found in 'packagename(s)' (comma-separated list");
            System.out.println();
            System.exit(1);
        }
    }

    /**
     * compares two strings. The following order is used:<br/>
     * <ul>
     * <li>case insensitive</li>
     * <li>german umlauts (&auml; , &ouml; etc.) or other non-ASCII letters are
     * treated as special chars</li>
     * <li>special chars &lt; numbers &lt; letters</li>
     * </ul>
     */
    public static class StringCompare implements Comparator<String> {

        /**
         * appends blanks to the string if its shorter than <code>len</code>.
         * 
         * @param s   the string to pad
         * @param len the minimum length for the string to have
         * @return the padded string
         */
        private String fillUp(String s, int len) {
            while (s.length() < len) {
                s += " ";
            }
            return s;
        }

        /**
         * returns the group of the character: 0=special char, 1=number, 2=letter.
         * 
         * @param c the character to check
         * @return the group
         */
        private int charGroup(char c) {
            int result;

            result = 0;

            if ((c >= 'a') && (c <= 'z')) {
                result = 2;
            } else if ((c >= '0') && (c <= '9')) {
                result = 1;
            }

            return result;
        }

        /**
         * Compares its two arguments for order.
         * 
         * @param o1 the first object
         * @param o2 the second object
         * @return -1 if o1&lt;o2, 0 if o1=o2 and 1 if o1&;gt;o2
         */
        @Override
        public int compare(String o1, String o2) {
            String s1;
            String s2;
            int i;
            int result;
            int v1;
            int v2;

            result = 0; // they're equal

            // get lower case string
            s1 = o1.toString().toLowerCase();
            s2 = o2.toString().toLowerCase();

            // same length
            s1 = fillUp(s1, s2.length());
            s2 = fillUp(s2, s1.length());

            for (i = 0; i < s1.length(); i++) {
                // same char?
                if (s1.charAt(i) == s2.charAt(i)) {
                    result = 0;
                } else {
                    v1 = charGroup(s1.charAt(i));
                    v2 = charGroup(s2.charAt(i));

                    // different type (special, number, letter)?
                    if (v1 != v2) {
                        if (v1 < v2) {
                            result = -1;
                        } else {
                            result = 1;
                        }
                    } else {
                        if (s1.charAt(i) < s2.charAt(i)) {
                            result = -1;
                        } else {
                            result = 1;
                        }
                    }

                    break;
                }
            }

            return result;
        }

        /**
         * Indicates whether some other object is "equal to" this Comparator.
         * 
         * @param obj the object to compare with this Comparator
         * @return true if the object is a StringCompare object as well
         */
        @Override
        public boolean equals(Object obj) {
            return (obj instanceof StringCompare);
        }

    }
}
